﻿module FunctionalAzure.TableStorage

open System
open System.IO
open System.Net
open System.Security.Cryptography
open System.Xml
open System.Xml.XPath
open System.Web
open Microsoft.FSharp.Collections

let create_functons user key = 
    let table_host = "ipv4.fiddler:10002"

    let getCanonicalizedResource (resource : string)=
        let delimiter_index = resource.IndexOf("?")
        if delimiter_index = -1 then
            String.Format("/{0}{1}", user, resource)
        else
            let splitters : char[] = "&".ToCharArray()
            let path = resource.Substring(0, delimiter_index)
            let data = Array.toList (resource.Substring(delimiter_index + 1).Split(splitters))
            let data_without_filters = List.filter (fun (x : string)-> not ((x.Chars 0) = '$')) data
            let get_key_val (x : string) = 
                let delimiter_index = x.IndexOf("=")
                (x.Substring(0, delimiter_index), x.Substring(delimiter_index + 1))
            let rec sort a = 
                if List.isEmpty a then
                    a
                else
                    let first (x, _) = x
                    let element = a.Head
                    let element_key = first(get_key_val element)
                    let less, more_or_equal = List.partition (fun x -> first(get_key_val x) < element_key) a.Tail
                    let more = List.filter (fun x -> not (x = element_key)) more_or_equal
                    (sort less)@[element]@(sort more)
                    
            let sorted_params = sort data_without_filters
            if List.isEmpty data_without_filters then
                String.Format("/{0}{1}", user, path)
            else
                let decode_param p = 
                    let key, value = get_key_val p
                    HttpUtility.UrlDecode(key.ToLower()) + ":" + HttpUtility.UrlDecode(value)
                String.Format("/{0}{1}\n{2}", user, path, String.Join("\n", List.map decode_param sorted_params))            

    let stringToSign verb contentMD5 contentType date canonicalizedResource =
        System.String.Format("{0}\n{1}\n{2}\n{3}\n{4}", verb, contentMD5, contentType, date, canonicalizedResource)

    let getAuthorization (canonicalizedString : string) =
        let hasher = new HMACSHA256(System.Convert.FromBase64String(key))
        let data = System.Text.Encoding.UTF8.GetBytes(canonicalizedString)
        let signature = System.Convert.ToBase64String(hasher.ComputeHash(data))
        System.String.Format(System.Globalization.CultureInfo.InvariantCulture, "SharedKey {0}:{1}", user, signature)

    let getRequestObject request_method (uri : string) = 
        let request_path = String.Format("http://{0}{1}", table_host, uri)
        let current_date = System.DateTime.UtcNow.ToString("R", System.Globalization.CultureInfo.InvariantCulture)
        let contentMD5 = System.String.Empty
        let content_type = "application/atom+xml"
        let canonicalized_resource = getCanonicalizedResource uri
        let ms_version = "2009-09-19"
        let canonicalized_request = stringToSign request_method contentMD5 content_type current_date canonicalized_resource
        let auth_header = getAuthorization (canonicalized_request)
        let request : HttpWebRequest = downcast WebRequest.Create(request_path)
        request.Method <- request_method
        request.ProtocolVersion <- (System.Version(1, 1))
        request.ContentType <- content_type
        request.Method <- request_method
        request.Headers.Add("x-ms-date", current_date)
        request.Headers.Add("Authorization", auth_header)
        request.Headers.Add("DataServiceVersion", "1.0;NetFx")
        request.Headers.Add("MaxDataServiceVersion", "1.0;NetFx")
        request.Headers.Add("Accept-Charset", "UTF-8")
        request.Headers.Add("x-ms-version", ms_version)
        request

    let listOfTables () =
        let uri = "/devstoreaccount1/Tables"
        let request_method = "GET"
        let request = getRequestObject request_method uri
        let response : HttpWebResponse = downcast request.GetResponse()
        let reader = new StreamReader(response.GetResponseStream())
        let data = reader.ReadToEnd()
        let doc = XmlDocument()
        doc.LoadXml(data)
        let nsMgr = new XmlNamespaceManager(doc.NameTable)
        nsMgr.AddNamespace("atom", "http://www.w3.org/2005/Atom")
        nsMgr.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")
        nsMgr.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices")
        let nodes = doc.SelectNodes("/atom:feed/atom:entry/atom:content/m:properties/d:TableName", nsMgr)
        [for i in nodes -> i.InnerText]

    let createTable (table_name : string) =
        let uri = "/devstoreaccount1/Tables"
        let request_method = "POST"
        let request = getRequestObject request_method uri
        request.Headers.Add("Accept-Charset", "UTF-8")
        let request_data = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\"><title /><updated>2009-03-18T11:48:34.9840639-07:00</updated><author><name/></author><id/><content type=\"application/xml\"><m:properties><d:TableName>{0}</d:TableName></m:properties></content></entry>"
        let ready_data = System.Text.Encoding.UTF8.GetBytes(System.String.Format(request_data, box table_name))
        request.ContentLength <- int64 ready_data.Length
        let request_stream = request.GetRequestStream()
        request_stream.Write(ready_data, 0, ready_data.Length)
        let response : HttpWebResponse = downcast request.GetResponse()
        let dataReader = response.GetResponseStream()
        let streamReader = new StreamReader(dataReader)
        let str_response = streamReader.ReadToEnd()
        ()

    let deleteTable (table_name : string) = 
        let uri = String.Format("/devstoreaccount1/Tables('{0}')", table_name)
        let request_method = "DELETE"
        let request = getRequestObject request_method uri
        let response : HttpWebResponse = downcast request.GetResponse()
        let reader = new StreamReader(response.GetResponseStream())
        let data = reader.ReadToEnd()
        ()

    let filter (table_name : string) (expression : string) (keys : List<string>) = 
        let uri = String.Format("/devstoreaccount1/{0}()?{1}", table_name, expression)
        let request_method = "GET"
        let request = getRequestObject request_method uri
        let response : HttpWebResponse = downcast request.GetResponse()
        let reader = new StreamReader(response.GetResponseStream())
        let data = reader.ReadToEnd()
        let doc = new XmlDocument()
        doc.LoadXml(data)
        let nsMgr = new XmlNamespaceManager(doc.NameTable)
        nsMgr.AddNamespace("atom", "http://www.w3.org/2005/Atom")
        nsMgr.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")
        nsMgr.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices")
        printfn "%A" data
        let entities = doc.SelectNodes("/atom:feed/atom:entry", nsMgr)
        let getValues (xmlNode : XmlNode) = 
            let get_value key = xmlNode.SelectSingleNode("atom:content/m:properties/d:" + key, nsMgr).InnerText
            List.map get_value keys
        seq {for i in entities do yield (getValues i)}

    let get (table_name : string) (partition_key : string) (row_key : string) (keys : List<string>) = 
        let uri = String.Format("/devstoreaccount1/{0}(PartitionKey='{1}',RowKey='{2}')", table_name, partition_key, row_key)
        let request_method = "GET"
        let request = getRequestObject request_method uri
        let response : HttpWebResponse = downcast request.GetResponse()
        let reader = new StreamReader(response.GetResponseStream())
        let data = reader.ReadToEnd()
        let doc = XmlDocument()
        doc.LoadXml(data)
        let nsMgr = new XmlNamespaceManager(doc.NameTable)
        nsMgr.AddNamespace("atom", "http://www.w3.org/2005/Atom")
        nsMgr.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata")
        nsMgr.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices")
        let get_key key = 
            doc.SelectSingleNode("/atom:entry/atom:content/m:properties/d:" + key, nsMgr).InnerText
        List.map (fun x -> get_key x) keys

    // requires list (property_name * type_name * value)
    let insert (table_name : string) (key_value_list : List<string * string  * string>) = 
        let uri = "/devstoreaccount1/" + table_name
        let request_method = "POST"
        let request_template = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\"><title /><updated>{0}</updated><author><name /></author><id /><content type=\"application/xml\"><m:properties><d:Timestamp m:type=\"Edm.DateTime\">{0}</d:Timestamp>{1}</m:properties></content></entry>"
        let current_date = System.DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture) + "Z"
        let nodes = String.Concat(List.map (fun (pn, tn, va) -> String.Format("<d:{0} {1}>{2}</d:{0}>", pn, tn, va)) key_value_list)
        let data = String.Format(request_template, current_date, nodes)
        let request = getRequestObject request_method uri
        request.Headers.Add("Accept-Charset", "UTF-8")
        let ready_data = System.Text.Encoding.UTF8.GetBytes(data)
        request.ContentLength <- int64 ready_data.Length
        let request_stream = request.GetRequestStream()
        request_stream.Write(ready_data, 0, ready_data.Length)
        let response : HttpWebResponse = downcast request.GetResponse()
        let dataReader = response.GetResponseStream()
        let streamReader = new StreamReader(dataReader)
        let str_response = streamReader.ReadToEnd()
        true

    (listOfTables, createTable, deleteTable, insert, get, filter)